43 服务自省设计方案:新版本新方案

随着微服务架构的不断发展和普及,RPC 框架成为微服务架构中不可或缺的重要角色,Dubbo 作为 Java 生态中一款成熟的 RPC 框架也在随着技术的更新换代不断发展壮大。当然,传统的 Dubbo 架构也面临着新思想、新生态和新技术带来的挑战。

在微服务架构中,服务是基本单位,而 Dubbo 架构中服务的基本单位是 Java 接口,这种架构上的差别就会带来一系列挑战。从 2.7.5 版本开始,Dubbo 引入了服务自省架构,来应对微服务架构带来的挑战。具体都有哪些挑战呢?下面我们就来详细说明一下。

注册中心面临的挑战

在开始介绍注册中心面临的挑战之前,我们先来回顾一下前面课时介绍过的 Dubbo 传统架构以及这个架构中最核心的组件:

Drawing 0.png

Dubbo 核心架构图

结合上面这张架构图,我们可以一起回顾一下这些核心组件的功能。

  • Registry:注册中心。 负责服务地址的注册与查找,服务的 Provider 和 Consumer 只在启动时与注册中心交互。注册中心通过长连接感知 Provider 的存在,在 Provider 出现宕机的时候,注册中心会立即推送相关事件通知 Consumer。
  • Provider:服务提供者。 在它启动的时候,会向 Registry 进行注册操作,将自己服务的地址和相关配置信息封装成 URL 添加到 ZooKeeper 中。
  • Consumer:服务消费者。 在它启动的时候,会向 Registry 进行订阅操作。订阅操作会从 ZooKeeper 中获取 Provider 注册的 URL,并在 ZooKeeper 中添加相应的监听器。获取到 Provider URL 之后,Consumer 会根据 URL 中相应的参数选择 LoadBalance、Router、Cluster 实现,创建相应的 Invoker 对象,然后封装服务接口的代理对象,返回给上层业务。上层业务调用该代理对象的方法,就会执行远程调用。
  • Monitor:监控中心。 用于统计服务的调用次数和调用时间。Provider 和 Consumer 在运行过程中,会在内存中统计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。监控中心在上面的架构图中并不是必要角色,监控中心宕机不会影响 Provider、Consumer 以及 Registry 的功能,只会丢失监控数据而已。

通过前面对整个 Dubbo 实现体系的介绍,我们知道URL 是贯穿整个 Dubbo 注册与发现的核心。Provider URL 注册到 ZooKeeper 上的大致格式如下:

dubbo://192.168.0.100:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=groupA&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync&pid=59975&release=&side=provider&timestamp=1601390276192

其中包括 Provider 的 IP、Port、服务接口的完整名称、Dubbo 协议的版本号、分组信息、进程 ID 等。

我们常用的注册中心,比如,ZooKeeper、Nacos 或 etcd 等,都是中心化的基础设施。注册中心基本都是以内存作为核心存储,其内存使用量与服务接口的数量以及 Provider 节点的个数是成正比的,一个 Dubbo Provider 节点可以注册多个服务接口。随着业务发展,服务接口的数量会越来越多,为了支撑整个系统的流量增长,部署的 Dubbo Provider 节点和 Dubbo Consumer 节点也会不断增加,这就导致注册中心的内存压力越来越大

在生产环境中为了避免单点故障,在搭建注册中心的时候,都会使用高可用方案。这些高可用方案的本质就是底层的一致性协议,例如,ZooKeeper 使用的是 Zab 协议,etcd 使用的是 Raft 协议。当注册数据频繁发生变化的时候,注册中心集群的内部节点用于同步数据的网络开销也会增大

从注册中心的外部看,Dubbo Provider 和 Dubbo Consumer 都可以算作注册中心的客户端,都会与注册中心集群之间维护长连接,这也会造成一部分网络开销和资源消耗

在使用类似 ZooKeeper 的注册中心实现方案时,注册中心会主动将注册数据的变化推送到客户端。假设一个 Dubbo Consumer 订阅了 N 个服务接口,每个服务接口由 M 个 Provider 节点组成的集群提供服务,在 Provider 节点进行机器迁移的时候,就会涉及 M * N 个 URL 的更新,这些变更事件都会通知到每个 Dubbo Consumer 节点,这就造成了注册中心在处理通知方面的压力

总之,在超大规模的微服务落地实践中,从内存、网络开销、通知等多个角度看,注册中心以及整个 Dubbo 传统架构都受到了不少的挑战和压力。

Dubbo 的改进方案

Dubbo 从 2.7.0 版本开始增加了简化 URL的特性,从 URL 中抽出的数据会被存放至元数据中心。但是这次优化只是缩短了 URL 的长度,从内存使用量以及降低通知频繁度的角度降低了注册中心的压力,并没有减少注册中心 URL 的数量,所以注册中心所承受的压力还是比较明显的。

Dubbo 2.7.5 版本引入了服务自省架构,进一步降低了注册中心的压力。在此次优化中,Dubbo 修改成应用为粒度的服务注册与发现模型,最大化地减少了 Dubbo 服务元信息注册数量,其核心流程如下图所示:

Lark20201222-120323.png

服务自省架构图

上图展示了引入服务自省之后的 Dubbo 服务注册与发现的核心流程,Dubbo 会按照顺序执行这些操作(当其中一个操作失败时,后续操作不会执行)。

我们首先来看 Provider 侧的执行流程:

1.发布所有业务接口中定义的服务接口,具体过程与[第 41 课时]中介绍的发布流程相同;

2.发布 MetadataService 接口,该接口的发布由 Dubbo 框架自主完成;

3.将 Service Instance 注册到注册中心;

4.建立所有的 Service ID 与 Service Name 的映射,并同步到配置中心。

接下来,我们再来看Consumer 侧的执行流程:

5.注册当前 Consumer 的 Service Instance,Dubbo 允许 Consumer 不进行服务注册,所以这一步操作是可选的;

6.从配置中心获取 Service ID 与 Service Name 的映射关系;

7.根据 Service ID 从注册中心获取 Service Instance 集合;

8.随机选择一个 Service Instance,从中获取 MetadataService 的元数据,这里会发起 MetadataService 的调用,获取该 Service Instance 所暴露的业务接口的 URL 列表,从该 URL 列表中可以过滤出当前订阅的 Service 的 URL;

9.根据步骤 8 中获取的业务接口 URL 发起远程调用。

至于上图中涉及的一些新概念,为方便你理解,这里我们对它们的具体实现进行一个简单的介绍。

  • Service Name:服务名称,例如,在一个电商系统中,有用户服务、商品服务、库存服务等。
  • Service Instance:服务实例,表示单个 Dubbo 应用进程,多个 Service Instance 构成一个服务集群,拥有相同的 Service Name
  • Service ID:唯一标识一个 Dubbo 服务,由 ${protocol}:${interface}:${version}:${group} 四部分构成。

在有的场景中,我们会在线上部署两组不同配置的服务节点,来验证某些配置是否生效。例如,共有 100 个服务节点,平均分成 A、B 两组,A 组服务节点超时时间(即 timeout)设置为 3000 ms,B 组的超时时间(即 timeout)设置为 2000 ms,这样的话该服务就有了两组不同的元数据。

按照前面介绍的优化方案,在订阅服务的时候,会得到 100 个 ServiceInstance,因为每个 ServiceInstance 发布的服务元数据都有可能不一样,所以我们需要调用每个 ServiceInstance 的 MetadataService 服务获取元数据。

为了减少 MetadataService 服务的调用次数,Dubbo 提出了服务修订版本的优化方案,其核心思想是:将每个 ServiceInstance 发布的服务 URL 计算一个 hash 值(也就是 revision 值),并随 ServiceInstance 一起发布到注册中心;在 Consumer 端进行订阅的时候,对于 revision 值相同的 ServiceInstance,不再调用 MetadataService 服务,直接共用一份 URL 即可。下图展示了 Dubbo 服务修订的核心逻辑:

Lark20201222-120318.png

引入 Dubbo 服务修订的 Consumer 端交互图

通过该流程图,我们可以看到 Dubbo Consumer 端实现服务修订的流程如下。

  1. Consumer 端通过服务发现 API 从注册中心获取 Provider 端的 ServiceInstance 列表。
  2. 注册中心返回 100 台服务实例,其中 revision 为 1 的 ServiceInstance 编号是 0~49,revision 为 2 的 ServiceInstance 编号是 50~99。
  3. Consumer 端在这 100 台服务实例中随机选择一台,例如,选择到编号为 68 的 ServiceInstance。
  4. Consumer 端调用 ServiceInstance 68 暴露的 MetadataService 服务,获得其发布的 Dubbo 服务 URL 列表,并在本地内存中建立 revision 为 2 的服务 URL 列表缓存。
  5. Consumer 端再从剩余的 99 台服务实例中随机选择一台,例如,选中了 ServiceInstance 30,发现其 revision 值为 1,且本地缓存中没有 revision 为 1 的服务 URL 列表缓存。此时,Consumer 会如步骤 4 一样发起 MetadataService 调用,从 ServiceInstance 30 获取服务 URL 列表,并更新缓存。

由于此时的本地缓存已经覆盖了当前场景中全部的 revision 值,后续再次随机选择的 ServiceInstance 的 revision 不是 1 就是 2,都会落到本地缓存中,不会再次发起 MetadataService 服务调用。后续其他 ServiceInstance 的处理都会复用本地缓存的这两个 URL 列表,并根据 ServiceInstance 替换相应的参数(例如,host、port 等),这样即可得到 ServiceInstance 发布的完整的服务 URL 列表。

一般情况下,revision 的数量不会很多,那么 Consumer 端发起的 MetadataService 服务调用次数也是有限的,不会随着 ServiceInstance 的扩容而增长。这样就避免了同一服务的不同版本导致的元数据膨胀

总结

在本课时,我们重点介绍了 Dubbo 的服务自省架构的相关内容。

首先,我们一起复习了 Dubbo 的传统架构以及传统架构中基础组建的核心功能和交互流程。然后分析了 Dubbo 传统架构在超大规模微服务落地实践中面临的各项挑战和压力。最后,我们重点讲解了 Dubbo 2.7.5 版本之后引入的服务自省方案,服务自省方案可以很好地应对 Dubbo 面临的诸多挑战,并缓解基于 Dubbo 实现的、超大规模的微服务系统压力。在此基础上,我们还特别介绍了 Dubbo 服务修订方案是如何避免元数据膨胀的具体原理。